#!/bin/env python

from os import path as ospath
from copy import copy as clone
from pytz import timezone, utc
from datetime import datetime, timedelta
from configparser import ConfigParser
from metaimage import Format, Image
from metaimage.gmapi import LocationInfo
from metaimage.logging import Dummy, FileXML

## helper functions ##

def _isdir_(path):
    return path is None or ospath.isdir(path)


def _load_(path):
    try:
        log.begin('image file="', path, '"')
        return Image(path)
    except IOError as err:
        log.error('loading metadata failed ...ignoring')
        log.end('image')
        return None


def _match_(image, make, model):
    try:
        if make == image['Exif.Image.Make'].strip() \
        and model == image['Exif.Image.Model'].strip():
            return True 
    except Exception as err:
        pass
    return False


def _rename_(image, args):
    log.begin('rename')
    args = clone(args)
    oldpath = image.path

    if args.destination is None:
        args.destination = ospath.dirname(oldpath)
    if args.destination != '' and args.destination[-1] != '/':
        args.destination += '/'

    args.prefix = args.destination + args.prefix
    args.suffix = args.suffix + ospath.splitext(oldpath)[1].lower()

    try:
        newpath = args.namescheme(image, args)
        while not image.rename(str(newpath)):
            log.warn('file exists: ', newpath)
            newpath.renew()
        log.info('renamed: ', oldpath, ' -> ', newpath)
    except Exception as err:
        log.error('rename ', image.path, ' -> ', newpath, ' failed\n  ', err)
    log.end('rename')


def _save_(image):
    try:
        image.save()
        log.info('metadata updates saved')
        return True
    except Exception as err:
        log.error('writing metadata to ', image.path, ' failed\n', err)
        return False


## main functions ##

def adjust_time(args):
    try:
        offset = timedelta(seconds=args.seconds, minutes=args.minutes,
                           hours=args.hours, days=args.days)
    except OverflowError as err:
        print('timedelta out of range!', file=args.stderr)
        return 2

    if offset.total_seconds() == 0:
        print('timedelta is zero, nothing to do!', file=args.stdout)
        return 0

    log.begin('adjust ', 'timedelta="', offset, '"')

    # file loop
    for path in args.image_list:
        image = _load_(path)
        if image is None:
            continue

        if args.match is not None \
        and not _match_(image, args.match[0], args.match[1]):
            log.info('image does not match given make and model')
            log.end('image')
            continue

        # adjust 'DateTime' timestamp
        for key in image:
            if 'DateTime' in key:
                try:
                    timestamp  = datetime.strptime(image[key], Format.DateTime)
                    timestamp += offset
                    image[key] = timestamp.strftime(Format.DateTime)
                    log.info(key ,' = ', image[key])
                except Exception as err:
                    log.warn('setting ', key, ' failed\n', err)

        # adjust gps timestamp
        timestamp = image.get_gps_timestamp()
        if timestamp is not None:
            try:
                timestamp += offset
                image.set_gps_timestamp(timestamp)
                log.info('GPS timestamp adjusted: ',
                         timestamp.strftime(Format.DateTime), ' UTC')
            except Exception as err:
                log.warn('setting GPS timestamp failed\n', err)

        _save_(image)
        log.end('image')

    log.end('adjust')
    return 0



def apply_preset(args):
    log.begin('apply preset="', args.preset, '"')

    config = ConfigParser()
    config.optionxform=str
    config.read(args.preset)

    try:
        make  = config['camera']['make']
        model = config['camera']['model']
    except Exception as err:
        log.error('preset missing valid \'camera\' section')
        log.end('apply')
        return 1

    try: offset = config['camera']['utc_offset']
    except Exception as err: offset = None

    try: update = config['metadata.update']
    except Exception as err:
        update = None
        log.warn('preset missing valid \'metadata.update\' section')

    try:
        filename = config['filename']
    except Exception as err:
        filename = None
        log.warn('preset missing \'filename\' section')

    if filename is not None:
        if 'prefix' in filename: args.prefix = filename['prefix']
        else: args.prefix = ''
        if 'suffix' in filename: args.suffix = filename['suffix']
        else: args.suffix = ''

    if not _isdir_(args.destination):
        log.error('nonexisting destination folder: ', args.destination)
        log.end('apply')
        return 1

    # file loop
    for path in args.image_list:
        image = _load_(path)
        if image is None:
            continue

        # match camera
        if not _match_(image, make, model):
            log.info('preset not applicable for this image')
            log.end('image')
            continue

        # adjust timestamp
        if offset is not None:
            try:
                log.begin('timestamp offset="', offset, '"')
                offset = image.utc_offset(offset)
                log.info('utc timestamp: ', 
                         image.timestamp.strftime(Format.DateTime))
                log.end('timestamp')
            except Exception as err:
                log.warn('adjusting timestamp failed')

        # metadata update
        if update is not None:

            log.begin('metadata_update')
            for key in update:
                try:
                    if update[key] == 'DELETE':
                        del image[key]
                        log.info('deleted: ', key)
                        continue
                    elif update[key] == 'DATETIME':
                        image[key] = image.timestamp.strftime(Format.DateTime)
                    elif update[key] == 'OFFSET':
                        image[key] = (datetime.strptime(image[key],
                            Format.DateTime) - offset).strftime(Format.DateTime)
                    else:
                        image[key] = update[key]
                    log.info('updated: ', key, ' = ', image[key])
                except Exception as err:
                    log.warn('invalid update: ', key, ' = ', update[key])

            log.end('metadata_update')

        # rename
        if filename is not None:
            _rename_(image, args)

        _save_(image)
        log.end('image')

    log.end('apply')
    return 0



def create_preset(args):
    image = _load_(args.image)
    if image is None:
        print('loading metadata failed!', file=args.stderr)
        return 1

    try:
        make = image['Exif.Image.Make'].strip()
        model = image['Exif.Image.Model'].strip()
    except Exception as err:
        print('insufficient metadata!', file=args.stderr)
        return 2

    print('[camera]',
    '\nmake = ', make,
    '\nmodel = ', model,
    '\nutc_offset = ', args.offset,
    '\n\n[filename]',
    '\n# prefix = _' if args.prefix == '' else '\nprefix ='+args.prefix,
    '\n# suffix = _' if args.suffix == '' else '\nsuffix ='+args.suffix,
    '\n\n[metadata.update]',
    '\n# manually add exif updates in this section',
    '\n# Exif.Image.Copyright = \'your name here\'',
    '\nExif.Image.TimeZoneOffset = 0',
    '\nExif.Image.DateTime = DATETIME',
    '\nExif.Image.DateTimeOriginal = DATETIME',
    '\nExif.Photo.DateTimeDigitized = DATETIME',
    '\nExif.Photo.DateTimeOriginal = DATETIME',
    file=args.stdout, sep='')
    return 0



def geotag(args):
    try:
        if args.coordinates:
            latitude, longitude = args.query.split(',')[:2]
            location = LocationInfo.fromcoordinates(latitude, longitude,
                                    timezone=True, elevation=True,
                                    interact=args.interact)
        else:
            location = LocationInfo.fromaddress(args.query, timezone=True,
                                    elevation=True, interact=args.interact)
    except RuntimeError as err:
        print(err, file=args.stderr)
        return 1
    except (KeyboardInterrupt, EOFError) as err:
        print(file=args.stderr)
        return 2
    except (IndexError, ValueError) as err:
        print('invalid input!', file=args.stderr)
        return 3
    except Exception as err:
        print('address lookup failed:\n %s'%err, file=args.stderr)
        return 4

    log.begin('geotag query="', args.query, '"')

    latitude, longitude, elevation = location.coordinates()
    tzlocal = timezone(location.timezone())
    if elevation < 0:
        elevation = 0

    log.info('location: ', str(location.address()))
    log.info('latitude: ', latitude, ', longitude: ', longitude)
    log.info('elevation: ', elevation, ', timezone: ', tzlocal)

    string2time = lambda s: datetime.strptime(s, Format.DateTime)
    time2string = lambda d: d.strftime(Format.DateTime)

    # file loop
    for path in args.image_list:
        image = _load_(path)
        if image is None:
            continue

        if args.match is not None and\
        not _match_(image, args.match[0], args.match[1]):
            log.info('image does not match given make and model')
            log.end('image')
            continue

        if args.offset is not None:
            image.utc_offset(args.offset)

        if image.get_gps_location() is None or args.force:
            image.set_gps_location(latitude, longitude, elevation)
            image['Exif.GPSInfo.GPSSatellites'] = 'maps.googleapis.com'
            log.info('GPS location updated')
        else:
            log.warn('GPS location already present; update ignored')

        if image.get_gps_timestamp() is None or args.force:
            try:
                zulu  = image.timestamp.astimezone(utc)
                local = image.timestamp.astimezone(tzlocal)

                image.set_gps_timestamp(zulu)
                log.info('GPS timestamp updated: ', time2string(zulu), ' UTC')

                key = 'Exif.Image.TimeZoneOffset'
                image[key] = str(int((local.utcoffset()).total_seconds() / 60))

                log.begin('localize_timestamps offset="', image[key], '"')
                log.info(key, ' = ', image[key])

                for key in ['Exif.Image.DateTimeOriginal',
                            'Exif.Photo.DateTimeOriginal']:
                    try:
                        reference = string2time(image[key])
                        break
                    except Exception as err:
                        reference = None
                        continue

                for key in image:
                    if 'DateTime' in key:
                        if reference is not None:
                            delta = string2time(image[key]) - reference
                            image[key] = time2string(local + delta)
                        else:
                            image[key] = time2string(local)
                        log.info(key ,' = ', image[key])
                log.end('localize_timestamps')
            except ValueError as err:
                log.warn('updating GPS timestamp failed: utc offset unknown')
            except Exception as err:
                log.warn('localizing timestamps failed\n', err)
                log.end('localize_timestamps')
        else:
            log.warn('GPS timestamp already present; update ignored')

        _save_(image)
        log.end('image')

    log.end('geotag')
    return 0



def list_metadata(args):
    def _grep_(words, value):
        match = False
        for word in words:
            if word in value:
                match = True
                break
        return match

    for path in args.image_list:
        try: image = Image(path)
        except IOError: continue

        if args.match is not None and\
        not _match_(image, args.match[0], args.match[1]):
            continue

        match = False
        for key in image:
            if args.keys is not None and\
            not _grep_(args.keys.split(','), key):
                continue

            try: value = str(image[key]).strip()
            except: value = '(INVALID_VALUE)'
            
            if args.values is not None and\
            not _grep_(args.values.split(','), value):
                continue

            if len(value) > args.suppress > 0:
                value = '(VALUE_SUPPRESSED)'

            if args.imagename or len(args.image_list) > 1:
                print(path, ':', end=' ', file=args.stdout)

            print(key, '=', value, file=args.stdout)
            match = True

        if not match:
            print(path, ': no matching tags!', file=args.stderr)

    return 0



def rename(args):
    log.begin('batch-rename')

    if not _isdir_(args.destination):
        log.error('nonexisting destination folder: ', args.destination)
        log.end('batch_rename')
        return 1

    for path in args.image_list:
        image = _load_(path)
        if image is None:
            continue

        if args.match is not None and\
        not _match_(image, args.match[0], args.match[1]):
            log.info('image does not match given make and model')
            log.end('image')
            continue

        if args.offset is not None:
            image.utc_offset(args.offset)

        _rename_(image, args)
        log.end('image')

    log.end('batch-rename')
    return 0



def utc_util(args):
    zulu = datetime.utcnow().replace(microsecond=0)
    print('utc timestamp:\n', zulu.isoformat('/'), file=args.stdout)

    if args.timestamp is not None:
        try:
            timestamp = datetime.strptime(args.timestamp, '%Y-%m-%d/%H:%M:%S')
        except Exception as err:
            print('\nrequired format: YYYY-MM-DD/HH:MM:SS', file=args.stdout)
            return 1
        offset = float((timestamp - zulu).total_seconds()/60)
        print('\noffset:', offset, 'minutes', file=args.stdout)
    return 0



if __name__ == '__main__':
    from sys import exit
    from metaimage.commandline import parse_args

    args = parse_args()
    args.action = {
        'adjust' : adjust_time,
        'apply'  : apply_preset,
        'create' : create_preset,
        'geotag' : geotag,
        'list'   : list_metadata,
        'rename' : rename,
        'utc'    : utc_util
    }[args.action]

    try:
        log = FileXML(args.log, verbose=True)
    except IOError:
        print('error: unable to write logfile!', file=args.stderr)
        sys.exit(1)
    except AttributeError:
        log = Dummy()

    exit(args.action(args))
